﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;

using IdeaNMR.Shared.Bruker;
using IdeaNMR.Shared.Utils;

/* This is how the setup node looks like
<setup xmlns="http://www.blog.nmrplus.com/">
  <pulprog isLink="true">components.pulseprogram</pulprog>
  <command name="pl0">120</command>
  <command name="xcpr">parmode 2D</command>
  <spnam12 isLink="true">components.Gauss5_256</spnam12>
  <command name="1 td">1.43712386735357</command>
</setup>
*/
namespace IdeaNMR.Shared
{
    public class SetupScope : IPythonExecutionScope
    {
        private readonly XElement _content;
        private readonly XNamespace _namespace;
        private readonly string[] _fileParameterRegexArray = { @"pulprog", @"spnam", @"cpdprg", @"fq.list", @"script" };

        public Dictionary<string, string> Parameters { get; set; }

        private readonly XName _nParam;

        #region Python Export

        public void BrukerFileCommand(string commandName, string componentName)
        {
            string funcName = "BrukerFileCommand";
            if (string.IsNullOrEmpty(commandName))
                throw new ParameterNullException("Bruker Command name is null or empty.", funcName);

            if (_fileParameterRegexArray.Where(x => Regex.Match(commandName, x).Success).Count() != 1)
                throw new ParameterException("Command name is invalid. Only cpdprg[1-10], pulprog, spnam[1-10], fq[1-4]list & script is supported.", commandName, funcName);

            if (string.IsNullOrEmpty(componentName))
                throw new ParameterNullException("Component name is null or empty.", funcName);

            if (!Regex.Match(componentName, @"^[_\-a-zA-Z0-9\.]+$").Success)
                throw new ParameterException("Bad component name.", componentName, funcName);

            string[] nameSet = componentName.Split('.');
            if (nameSet.Length == 0 || nameSet.Length > 2)
                throw new ParameterException("Bad component name. It must be either single word component name or full component name 'components.<name>'.", componentName, funcName);

            string cN; // holds the full component name

            if (nameSet.Length == 2 && nameSet[0] == "components")
                cN = componentName;
            else if (nameSet.Length == 1)
                cN = "components." + componentName;
            else
                throw new ParameterException("Bad component name. It must be either single word component name or full component name 'components.<name>'.", componentName, funcName);

            XElement setup = GetParameterElement("setup");
            if (setup == null)
            {
                _content.Add(new XElement(_namespace + "param", new XAttribute("name", "setup")));
                setup = GetParameterElement("setup");
            }
            setup.Add(new XElement(_namespace + "param",
                new XAttribute("name", commandName),
                new XAttribute("value", cN),
                new XAttribute("command", true),
                new XAttribute("file", true)));
        }

        public void BrukerCommand(string commandName, string parameterValue)
        {
            string funcName = "BrukerCommand";

            if (string.IsNullOrEmpty(commandName))
                throw new ParameterNullException("Command name is null or empty.", funcName);

            if (parameterValue == null) parameterValue = string.Empty;

            XElement setup = GetParameterElement("setup");
            
            if (setup == null)
            {
                _content.Add(new XElement(_namespace + "param", new XAttribute("name", "setup")));
                setup = GetParameterElement("setup");
            }
            /* This is how the setup node looks like
            <setup xmlns="http://www.blog.nmrplus.com/">
              <pulprog isLink="true">components.pulseprogram</pulprog>
              <command name="pl0">120</command>
              <command name="xcpr">parmode 2D</command>
              <spnam12 isLink="true">components.Gauss5_256</spnam12>
              <command name="1 td">1.43712386735357</command>
            </setup>
            */
            setup.Add(new XElement(_namespace + "param",
                            new XAttribute("name", commandName),
                            new XAttribute("value", parameterValue),
                            new XAttribute("command", true)));
        }

        public void BrukerCommand(string commandName, double parameterValue)
        {
            // calls the BrukerCommand method with 3 arguments below, with arbitrary precision of 6
            this.BrukerCommand(commandName, parameterValue, 6);
        }

        public void BrukerCommand(string commandName, double parameterValue, int precision)
        {
            // rounds double based on precision, converts value to a string
            // then calls the BrukerCommand(string,string) method above
            this.BrukerCommand(commandName, Math.Round(parameterValue, precision).ToString());
        }

        /// <summary>
        /// Adds a parameter to the scope if parameter with this name exists its value get updated. 
        /// </summary>
        /// <param name="parameterName">Full name of parameter.</param>
        /// <param name="parameterValue">Value of parameter.</param>
        /// <returns>true - if added or changed value, false if parameter with the same 
        /// name and value exists.</returns>
        public void AddParameter(string parameterName, string parameterValue)
        {
            string funcName = "AddParameter";

            {
                Match parameterElements = Regex.Match(parameterName, @"^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*$");
                if (!parameterElements.Success)
                    throw new ParameterException("Bad parameter name.", parameterName, funcName);
            }

            string[] nameSet = parameterName.Split('.');
            // Check if parameter exists

            XElement cElem = _content;

            XName nParam;
            nParam = _namespace + "param";

            for (int i = 0; i < nameSet.Length; i++)
            {
                var elements = cElem.Elements().Where<XElement>(x => x.Attribute("name") != null).Where(x => x.Attribute("name").Value == nameSet[i]);
                if (elements.Count() == 0)
                {
                    XElement nElem = new XElement(nParam, new XAttribute("name", nameSet[i]));
                    cElem.Add(nElem);
                    cElem = nElem;
                }
                else
                {
                    cElem = elements.First();
                    if (cElem.Attribute("command") != null)
                    {
                        throw new ParameterException("Cannot add a child parameter to BrukerCommand.", parameterName, funcName);
                    }
                }

            }

            cElem.SetAttributeValue("value", parameterValue);
        }

        public void AddParameter(string parameterName, double parameterValue)
        {
            this.AddParameter(parameterName, parameterValue.ToString());
        }

        public void LinkFields(string sourceField, string targetField)
        {
            string funcName = "LinkFields";

            if (string.IsNullOrEmpty(sourceField))
                throw new ParameterNullException("Source parameter is null or empty.", funcName);

            if (string.IsNullOrEmpty(targetField))
                throw new ParameterNullException("Target parameter is null or empty.", funcName);

            XElement checkTarget = GetParameterElement(targetField);
            if (checkTarget == null) throw new ParameterNotFoundException("Target parameter is not found.", targetField, funcName);

            this.AddParameter(sourceField, targetField);

            XElement newLink = GetParameterElement(sourceField);
            if (newLink == null) throw new ParameterException("Cannot create a source parameter. Please report this error.", sourceField, funcName);

            newLink.RemoveNodes(); // This is essential for that the would be no circular links

            XAttribute linkAttribute = new XAttribute("href", targetField);
            newLink.Add(linkAttribute);
        }

        /// <summary>
        /// Gets the value of the parameter.
        /// </summary>
        /// <param name="parameterName">Parameter name</param>
        /// <returns>String containing value of the parameter or null.</returns>
        /// <exception cref="System.ArgumentNullException">If parameter does not exists.</exception>
        public object GetValue(string parameterName)
        {
            string funcName = "GetValue";

            if (string.IsNullOrEmpty(parameterName)) throw new ParameterNullException("Parameter name is null or empty.", funcName);

            string[] nameSet = parameterName.Split('.');
            if (nameSet.Length < 1)
                throw new ParameterException("Bad parameter", parameterName, funcName);

            XElement currentElement = GetParameterElement(parameterName);
            if (currentElement == null) throw new ParameterException("Cannot find a parameter.", parameterName, funcName);

            string theValue = null;
            if (currentElement.Attribute("value") != null) theValue = currentElement.Attribute("value").Value;
            else throw new ParameterException("Parameter has no value defined.", parameterName, funcName);

            int numericI;
            if (int.TryParse(theValue, out numericI)) return numericI;

            double numericF;
            if (double.TryParse(theValue, out numericF)) return numericF;

            return theValue;
        }

        public object ReadUserParameter(string parameterName)
        {
            string funcName = "ReadUserParameter";
            if (string.IsNullOrWhiteSpace(parameterName))
                throw new ParameterNullException("Parameter is null or empty or contains only white spaces.", funcName);

            if (this.Parameters.ContainsKey(parameterName))
            {
                string theValue = this.Parameters[parameterName];

                int numericI;
                if (int.TryParse(theValue, out numericI)) return numericI;

                double numericF;
                if (double.TryParse(theValue, out numericF)) return numericF;

                return theValue;
            }
            else
            {
                string err = string.Format("Parameter {0} is not found in user input parameters string.", parameterName);
                throw new ParameterNotFoundException(err, parameterName, funcName);
            }
        }
        #endregion

        public IPythonExecutionScope GetSecureScope()
        {
            IPythonExecutionScope finalResult = new SecuredSetupScope(this);
            return finalResult;
        }

        public static SetupScope OpenSecuredSetupScope(SecuredSetupScope sss)
        {
            SetupScope finalResult = sss._secureContent;
            return finalResult;
        }

        #region Add Component
        public void AddComponent(string uniqueName, string fileName, string content)
        {
            if (string.IsNullOrWhiteSpace(uniqueName)) throw new ArgumentNullException("Component name cannot be null");
            if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException("Component filename cannot be null");
            if (content == null) throw new ArgumentNullException("Component content cannot be null");

            string parName;
            // Add component
            parName = "components." + uniqueName;
            AddParameter(parName, string.Empty);

            // Add filename
            string fPar = parName + ".filename";
            AddParameter(fPar, fileName);

            // Add content
            string cPar = parName + ".content";
            AddParameter(cPar, content);

            //            XElement component = new XElement(_namespace + uniqueName,
            //            new XAttribute("isFile", "yes"),
            //           new XElement(_namespace + "filename", fileName),
            //           new XElement(_namespace + "content", content));

            //          _content.Element(_namespace + "components").Add(component);
        }
        #endregion

        #region Generate Experiment Script
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref=""></exception>
        public string CreateScript(string scriptName, string userName, string ppName)
        {
            // Host dependant variables
            string parHomeDir = (string)GetValue("spectrometer.software.homedir"); // To copy files
            string pathSeparator = ((string)GetValue("spectrometer.software.os")).ToLower().Contains("windows") == true ? "\\\\" : "/";
            OSType hostOS = ((string)GetValue("spectrometer.software.os")).ToLower().Contains("windows") == true ? OSType.Windows : OSType.Unix;
            string newLineCharacter = null;

            // getting new line charachter for target system
            switch (hostOS)
            {
                case OSType.Windows:
                    newLineCharacter = "\r\n";
                    break;
                case OSType.Unix:
                    newLineCharacter = "\n";
                    break;
                case OSType.Mac:
                    newLineCharacter = "\r";
                    break;
                default:
                    newLineCharacter = "\n";
                    break;
            }

            string uniqueFileID = Miscellaneous.GetUniqueFileName(userName);

            StringBuilder scriptTemplate = new StringBuilder();
            #region Get Template File From Assembly
            {
                Assembly cAssembly = Assembly.GetExecutingAssembly();
                StreamReader textReader = new StreamReader(cAssembly.GetManifestResourceStream("IdeaNMR.Shared.ScriptTemplate.py"));
                scriptTemplate.Append(textReader.ReadToEnd());
            }
            #endregion

            scriptTemplate.Replace("##SCRIPTNAME##", scriptName + ".py");

            StringBuilder fileList = new StringBuilder();

            // This will be used to append new file entry to output python script
            string fileTemplate = "\t\tself.fileList.append(File(\"{0}\", \"\"\"{1}\"\"\", \"\"\"{2}\"\"\"))\n";

            // All Descendants of setup element for faster access
            IEnumerable<XElement> setupPars = null;
            try
            {
                XElement setupElem = GetParameterElement("setup");
                setupPars = GetParameterElement("setup").Descendants();
            }
            catch (Exception ex)
            {
                ParameterException pex = new ParameterException("The bruker setup is not formed. Use BrukerCommand and BrukerFileCommand to form bruker macro.", ex);
                throw pex;
            }

            #region Generate Mac File
            StringBuilder macroFile = new StringBuilder();
            // XCPR goes first

            try
            {
                string xcpr = (string)GetValue("setup.xcpr");
                macroFile.AppendFormat("{0} {1}\n", "xcpr", xcpr);
            }
            catch (ParameterException)
            { }
            // Getting all commands
            var commands = from com in setupPars
                           where com.Attribute("name") != null && com.Attribute("command") != null
                           where com.Attribute("name").Value.ToLowerInvariant() != "xcpr" && com.Attribute("command").Value == "true"
                           select com;


            Dictionary<string, fileEntry> distinctFiles = new Dictionary<string, fileEntry>();

            fileEntry fE;
            foreach (XElement x in commands)
            {
                if (string.IsNullOrEmpty(x.Attribute("name").Value))
                    throw new ArgumentException("Parameter name is null or empty.");

                // Important Attributes
                string n, v;   // Name, value
                bool c, f; // command, file

                n = (x.Attribute("name") != null) ? x.Attribute("name").Value : string.Empty;
                v = (x.Attribute("value") != null) ? x.Attribute("value").Value : string.Empty;
                c = x.Attribute("command") != null && x.Attribute("command").Value == "true";
                f = x.Attribute("file") != null && x.Attribute("file").Value == "true";

                string val;
                if (f)
                {
                    fE = new fileEntry();

                    val = v.Split('.').Last<string>() + uniqueFileID;
                    if (n.Contains("cpdprg"))
                    {
                        val = FixBrukerBug.FixCPDPRGFilenameLength(val);
                        fE.Directory = (string)GetValue("spectrometer.software.homedir.cpdprg");
                        if (fE.Directory == null)
                            fE.Directory = parHomeDir + pathSeparator + "cpd" + pathSeparator;
                    }
                    else if (n.Contains("pulprog"))
                    {
                        val = ppName + uniqueFileID;
                        fE.Directory = (string)GetValue("spectrometer.software.homedir.pulprog");
                        if (fE.Directory == null)
                            fE.Directory = parHomeDir + pathSeparator + "pp" + pathSeparator;
                    }
                    else if (n.Contains("spnam"))
                    {
                        fE.Directory = (string)GetValue("spectrometer.software.homedir.spnam");
                        if (fE.Directory == null)
                            fE.Directory = parHomeDir + pathSeparator + "wave" + pathSeparator;
                    }
                    else if (Regex.Match(n, "fq[1-4]list").Success)
                    {
                        fE.Directory = (string)GetValue("spectrometer.software.homedir.fq1list");
                        if (fE.Directory == null)
                            fE.Directory = parHomeDir + pathSeparator + "f1" + pathSeparator;
                    }
                    else if (Regex.Match(n, "script").Success)
                    // This option must not create a line in macroFile
                    {
                        fE.Directory = (string)GetValue("spectrometer.software.homedir.scripts");
                        if (fE.Directory == null)
                            fE.Directory = parHomeDir + pathSeparator + "scripts" + pathSeparator;
                    }
                    fE.Content = (string)GetValue(v + ".content");
                    fE.FileName = val;

                    try
                    {
                        distinctFiles.Add(fE.FileName, fE); // Throws if the same file added twice
                    }
                    catch (ArgumentException) { }

                    if (Regex.Match(n, "script").Success) continue;
                }
                else
                {
                    val = v;
                }
                macroFile.AppendFormat("{0} {1}\n", n, val);
            }

            string homeDir = (string)GetValue("spectrometer.software.homedir.macro");
            if (homeDir == null) homeDir = parHomeDir + pathSeparator + "mac" + pathSeparator;
            fileList.AppendFormat(fileTemplate, scriptName + uniqueFileID + ".mac",
                homeDir.Replace("\\", "\\\\"), macroFile.ToString().Replace("\n", newLineCharacter));

            foreach (fileEntry fEntry in distinctFiles.Values)
            {
                fileList.AppendFormat(fileTemplate, fEntry.FileName, fEntry.Directory.Replace("\\", "\\\\"), fEntry.Content);
            }

            #endregion

            #region Generate Component FileList

            scriptTemplate.Replace("#__FILELIST__#", fileList.ToString());
            #endregion

            #region ConvertNew Line Charachter To System Specific
            string newLineTemp = "##__NEWLINE__##";
            scriptTemplate.Replace("\r\n", newLineTemp);
            scriptTemplate.Replace("\n", newLineTemp);
            scriptTemplate.Replace("\r", newLineTemp);
            scriptTemplate.Replace(newLineTemp, newLineCharacter);
            #endregion
            return scriptTemplate.ToString();
        }

        private struct fileEntry
        {
            public string FileName;
            public string Content;
            public string Directory;
        }

        #endregion

        // ALISTAIR: comments added 10/07/2011
        // This method is not explicitly called 
        // anywhere within IdeaNMR!!!
        public void LoadParameterSet(string sourceSet)
        {
            string[] parametersNameValuePairs = sourceSet.Split('\n');

            for (int i = 0; i < parametersNameValuePairs.Length; i++)
            {
                string[] nameValuePair = parametersNameValuePairs[i].Split('=');
                if (nameValuePair.Length != 2) continue;
                string cName = nameValuePair[0].Trim();
                string cValue = nameValuePair[1].Trim();
                if ((cName.Length == 0) || (cValue.Length == 0)) continue;
                AddParameter(cName, cValue);
            }

            return;
        }

        /// <summary>
        /// Returns an XElement for the specified parameter name or null if not found.
        /// </summary>
        /// <param name="parName"></param>
        /// <returns>Found XElement or null if not found.</returns>
        /// <exception cref="IdeaNMR.Shared.ParameterException">When input parameter name is null or empty.</exception>
        private XElement GetParameterElement(string parName)
        {
            string funcName = "GetParameterElement";

            XElement cElement = _content;
            if (parName == string.Empty) throw new ParameterException("Parameter is empty string. Please report this problem.", null, funcName);

            string[] nameSet = parName.Split('.');
            if (nameSet.Length < 1)
                throw new ParameterException("Bad parameter. Please report this problem", parName, funcName);

            foreach (string iterName in nameSet)
            {
                var elements = cElement.Elements(_nParam).Where(x => x.Attribute("name") != null).Where(x => x.Attribute("name").Value == iterName);

                if (elements.Count() == 0) return null;

                cElement = elements.First();

                if (cElement.Attribute("href") != null)
                    cElement = GetParameterElement(cElement.Attribute("href").Value);
                if (cElement == null) return null;
            }

            return cElement;
        }

        public override string ToString()
        {
            return _content.ToString();
        }

        public string ToString(string elementName)
        {
            XElement requiredElement = GetParameterElement(elementName);
            if (requiredElement == null) return string.Format("{0} not found.", elementName);

            return requiredElement.ToString();
        }

        public SetupScope(string rootName, string documentNamespace)
        {
            _namespace = documentNamespace;
            _nParam = _namespace + "param";
            _content = new XElement(_namespace + rootName);
        }

        // ALISTAIR: comments added 10/07/2011
        // If the SetupScope constructor is called with only
        // the rootName argument, then call the constructor with
        // two arguments (above), using "http://www.blog.nmrplus.com/"
        // as the default document namespace
        public SetupScope(string rootName)
            : this(rootName, "http://www.blog.nmrplus.com/")
        { }
    }

    /// <summary>
    /// Limits the SetupScope Class to IPythonExecutionScope.
    /// Essentially that simple polymorphism cannot be used to cast from 
    /// IPythonExecutionScope to SetupScope from withing a PythonEngine.
    /// So, no functions of SetupScope are available to Python Interpreter
    /// except those defined by interface.
    /// </summary>
    public class SecuredSetupScope : IPythonExecutionScope
    {
        // Must be accesible to SetupScope.OpenSecuredSetupScope
        // and closed to anything else especially PythonInterpreter Assembly
        internal SetupScope _secureContent;

        public SecuredSetupScope(SetupScope insecureSetupScope)
        {
            _secureContent = insecureSetupScope;
        }

        #region IPythonExecutionScope Members

        // ALISTAIR: comments added 10/07/2011
        // Each method simply calls the _secureContent version of itself

        public void BrukerFileCommand(string commandName, string componentName)
        {
            _secureContent.BrukerFileCommand(commandName, componentName);
        }

        public void BrukerCommand(string commandName, string parameterValue)
        {
            _secureContent.BrukerCommand(commandName, parameterValue);
        }

        public void BrukerCommand(string commandName, double parameterValue)
        {
            _secureContent.BrukerCommand(commandName, parameterValue);
        }

        public void BrukerCommand(string commandName, double parameterValue, int precision)
        {
            _secureContent.BrukerCommand(commandName, parameterValue, precision);
        }

        public void AddParameter(string parameterName, string parameterValue)
        {
            _secureContent.AddParameter(parameterName, parameterValue);
        }

        public void AddParameter(string parameterName, double parameterValue)
        {
            _secureContent.AddParameter(parameterName, parameterValue);
        }

        public void LinkFields(string sourceField, string targetField)
        {
            _secureContent.LinkFields(sourceField, targetField);
        }

        public object GetValue(string parameterName)
        {
            return _secureContent.GetValue(parameterName);
        }
        public object ReadUserParameter(string parameterName)
        {
            return _secureContent.ReadUserParameter(parameterName);
        }

        #endregion
    } // end SecuredSetupScope class

    // To facilitate consistency of code.
    // ALISTAIR: comments added 10/07/2011
    // Contains signature declarations of all the
    // methods defined in SecuredSetupScope
    public interface IPythonExecutionScope
    {
        void BrukerFileCommand(string commandName, string componentName);

        void BrukerCommand(string commandName, string parameterValue);
        void BrukerCommand(string commandName, double parameterValue);
        void BrukerCommand(string commandName, double parameterValue, int precision);

        void AddParameter(string parameterName, string parameterValue);
        void AddParameter(string parameterName, double parameterValue);

        void LinkFields(string sourceField, string targetField);
        object GetValue(string parameterName);

        object ReadUserParameter(string parameterName);
    }

    /// <summary>
    /// Predefined functions for Python Interpreter
    /// </summary>
    public class PythonScopeFunctions
    {
        public static string Channels(string pulseProgram)
        {
            return pulseProgram;
        }
    }
}
